﻿using System.Runtime.InteropServices;
using Certify.Client;
using Certify.Management;
using Certify.Models;
using Certify.Models.Reporting;
using Certify.Server.Core;
using Certify.Server.Hub.Api.Extensions;
using Certify.Server.Hub.Api.Middleware;
using Certify.Server.Hub.Api.Services;
using Certify.Server.Hub.Api.SignalR;
using Certify.Server.Hub.Api.SignalR.ManagementHub;
using Certify.Server.HubService.Services;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.OpenApi;
using Scalar.AspNetCore;
using Serilog;

List<ActionStep> _systemStatusItems = [];
void AddSystemStatusItem(string systemStatusCategory, string systemStatusKey, string title, string description, bool hasError = false, bool hasWarning = false) => _systemStatusItems.Add(new ActionStep(systemStatusKey, systemStatusCategory, title, description, hasError, hasWarning));

var hubServiceAssembly = typeof(Certify.Server.HubService.Services.CertifyDirectHubService).Assembly;

// allow settings to be loaded from the app data path, that way settings are preserved between re-installs, copy a default config so service starts on localhost:8080
var settingsPath = EnvironmentUtil.EnsuredAppDataPath();
var hubSettings = Path.Combine(settingsPath, "hubservice.json");

// set working directory so that when we are started as a service we can find our config
var cwd = Path.GetDirectoryName(hubServiceAssembly.Location);

if (cwd != null)
{
    System.Diagnostics.Debug.WriteLine($"Using working directory {cwd}");
    Directory.SetCurrentDirectory(cwd);

#if !DEBUG

    // copy the default settings if they don't exist yet, then generate a new JWT issuer secret
    var defaultHubSettings = Path.Combine(cwd, "default-settings.json");

    if (!File.Exists(hubSettings) && File.Exists(defaultHubSettings))
    {
        var content = File.ReadAllText(defaultHubSettings);

        var secret = Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(32));

        content = content.Replace("<replace jwt secret>", secret);

        // copy default config if it doesn't exist
        File.WriteAllText(hubSettings, content);
    }
#endif
}
else
{
    System.Diagnostics.Debug.WriteLine($"Could not determine working directory");
}

var builder = WebApplication.CreateBuilder(args);

// load optional config but ignore errors if it doesn't exist or is invalid, otherwise service will fail to start
if (hubSettings != null && File.Exists(hubSettings))
{
    try
    {
        builder.Configuration.AddJsonFile(hubSettings, optional: true, reloadOnChange: true);
    }
    catch (Exception ex)
    {
        // ignore errors loading config, we will log them later
        AddSystemStatusItem(
            SystemStatusCategories.HUB_API,
            SystemStatusKeys.HUB_API_STARTUP_CUSTOMCONFIG,
            title: "Hub API Service Custom Config",
            description: $"Error loading config file {hubSettings} - {ex}",
            hasError: true
        );
    }
}

// if windows, run as service, otherwise run as console app
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
    builder.Services.AddWindowsService()
                    .AddHostedService<AgentBackgroundService>();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
    builder.Services.AddSystemd()
                .AddHostedService<AgentBackgroundService>();
}

builder.AddServiceDefaults();

AddSystemStatusItem(
    SystemStatusCategories.HUB_API,
    SystemStatusKeys.HUB_API_MODE,
    title: "Hub API with integrated Primary Instance",
    description: "Hub API using directly integrated primary service."
);

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(builder =>
    {
        builder.AllowAnyOrigin().AllowAnyMethod();
    });
});

builder.Services
    .AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true))
    .AddMemoryCache()
    .AddTokenAuthentication(builder.Configuration)
    .AddAuthorization()
    .AddControllers()
    .ConfigureApplicationPartManager((apm) =>
    {
        // remove service core assembly part, as controllers from this assembly are not needed in the hub API
        var serviceCore = (apm.ApplicationParts.FirstOrDefault(p => p.Name == "Certify.Server.Core") as AssemblyPart);
        if (serviceCore != null)
        {
            apm.ApplicationParts.Remove(serviceCore);
        }
    });

builder.Services
    .AddRouting(r => r.LowercaseUrls = true)
    .AddResponseCompression(opts =>
     {
         opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
             new[] { "application/octet-stream", "application/json" });
     })
    .AddSignalR(opt => opt.MaximumReceiveMessageSize = null).AddMessagePackProtocol();

var appDataPath = EnvironmentUtil.EnsuredAppDataPath("keys");

builder.Services
    .AddDataProtection(a =>
    {
        a.ApplicationDiscriminator = "certify";
    })
    .PersistKeysToFileSystem(new DirectoryInfo(appDataPath));

// configure OpenAPI
builder.Services.AddOpenApi("v1", options =>
{
    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0;

    // Add document information
    var info = new OpenApiInfo
    {
        Title = "Certify Management Hub API",
        Version = "v1",
        Description = "The Certify Management Hub API provides a certificate services API for use in UI, devops, CI/CD, middleware etc. See certifytheweb.com for more details."
    };

    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        document.Info = info;

        // Add security schemes - JWT Bearer OR API Key authentication (not both)
        document.Components ??= new OpenApiComponents();
        document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();

        // JWT Bearer authentication scheme
        document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
        {
            Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
            Name = "Authorization",
            In = ParameterLocation.Header,
            Type = SecuritySchemeType.Http,
            Scheme = "bearer",
            BearerFormat = "JWT"
        };

        // API Key authentication scheme (Client ID + Secret)
        document.Components.SecuritySchemes["ApiKey"] = new OpenApiSecurityScheme
        {
            Description = "API Key authentication using X-Client-ID and X-Client-Secret headers",
            Type = SecuritySchemeType.ApiKey,
            In = ParameterLocation.Header,
            Name = "X-Client-ID"
        };

        // Define security requirements - use either Bearer OR ApiKey (not both)
        document.Security = new List<OpenApiSecurityRequirement>
        {
            // Option 1: JWT Bearer token
            new() {
                [new OpenApiSecuritySchemeReference("Bearer", document)] = []
            },
            // Option 2: API Key (Client ID + Secret)
            new() {
                 [new OpenApiSecuritySchemeReference("ApiKey", document)] = []
            }
        };

        return Task.CompletedTask;
    });

    // Add operation customization for XML comments
    options.AddOperationTransformer((operation, context, cancellationToken) =>
    {
        // Use the actual method names as the generated operation id
        if (context.Description.ActionDescriptor.RouteValues.TryGetValue("action", out var action))
        {
            operation.OperationId = action;
        }

        // Ensure each operation has the security requirements from the document level
        // This will show both authentication options in Scalar/Swagger UI
        if (operation.Security == null || operation.Security.Count == 0)
        {
            operation.Security = new List<OpenApiSecurityRequirement>
            {
                // Option 1: JWT Bearer token
                new() {
                [new OpenApiSecuritySchemeReference("Bearer")] = []
            },
                // Option 2: API Key (Client ID + Secret)
             new() {
                [new OpenApiSecuritySchemeReference("ApiKey")] = []
            },
            };
        }

        return Task.CompletedTask;
    });

    // Add schema transformer for FileContentResult
    options.AddSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.JsonTypeInfo.Type == typeof(FileContentResult))
        {
            schema.Type = JsonSchemaType.String;
            schema.Format = "binary";
        }

        return Task.CompletedTask;
    });

});

builder.Services.AddEndpointsApiExplorer();

// add an internal config store for hub api internal use (acme config etc)
var acmeStore = new Certify.Datastore.SQLite.SQLiteConfigurationStore("acme-server", customDbFileName: "acme-config");
await acmeStore.PerformMaintenance();

var acmeServerState = new AcmeServerConfig(acmeStore, "acme-server");
builder.Services.AddSingleton<AcmeServerConfig>(acmeServerState);
builder.Services.AddAcmeServices();

// Register proxy provider and HTTP client provider
builder.Services.AddSingleton<Certify.Shared.Net.IProxyProvider>(sp =>
{
    return new Certify.Shared.Net.ProxyProvider(() =>
    {
        // For Hub Service, default to environment proxy
        return new Certify.Models.Preferences
        {
            ProxyMode = Certify.Models.ProxyMode.Environment,
            ProxyEnabled = true
        };
    });
});

builder.Services.AddSingleton<Certify.Models.Providers.IHttpClientProvider, Certify.Shared.Net.HttpClientProvider>();

// setup public/hub api
builder.Services.AddSingleton<Certify.Management.ICertifyManager, Certify.Management.CertifyManager>();

builder.Services.AddTransient(typeof(ICertifyInternalApiClient), typeof(CertifyDirectHubService));

// setup server core
builder.Services.AddSingleton<IInstanceManagementStateProvider, InstanceManagementStateProvider>();

builder.Services.AddTransient<ManagementAPI>();

// used to directly talk back to the management server process instead of connecting back via SignalR
builder.Services.AddTransient<IInstanceManagementHub, InstanceManagementHub>();

builder.Services.AddTransient<IManagementServerClient, DirectManagementServerClient>();

builder.Services.AddHostedService<ManagementWorker>();

// build app and configure aspnet middleware

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();

    AddSystemStatusItem(
        SystemStatusCategories.HUB_API,
        SystemStatusKeys.HUB_API_STARTUP_ENVIRONMENT,
        title: "Development Mode",
        description: $"Hub API is in Development mode."
    );
}
else
{
    AddSystemStatusItem(
        SystemStatusCategories.HUB_API,
        SystemStatusKeys.HUB_API_STARTUP_ENVIRONMENT,
        title: "Production Mode",
        description: $"Hub API is in Production mode."
    );
}

app.MapDefaultEndpoints();

//app.UseHttpsRedirection();

app.UseResponseCompression();

// Rewrite /ui/* to / so SPA default file (index.html) is served
// https://learn.microsoft.com/aspnet/core/fundamentals/middleware
app.Use((context, next) =>
{
    if ((HttpMethods.IsGet(context.Request.Method) || HttpMethods.IsHead(context.Request.Method))
        && (context.Request.Path.StartsWithSegments("/ui") || context.Request.Path.StartsWithSegments("/authentication")))
    {
        context.Request.Path = "/";
    }

    return next(context);
});

// serve static files from wwwroot
app.UseDefaultFiles();

// Set up custom content types - associating file extension to MIME type
var provider = new FileExtensionContentTypeProvider();
// Add new mappings
provider.Mappings[".dat"] = "application/octet-stream";
provider.Mappings[".dll"] = "application/octet-stream";
provider.Mappings[".br"] = "application/x-br";
provider.Mappings[".image"] = "image/png";

app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = provider
});

// configure CORS
app.UseCors((p) =>
{
    p.AllowAnyOrigin()
    .AllowAnyMethod()
    .AllowAnyHeader();
});

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.MapHub<UserInterfaceStatusHub>("/api/internal/status");
app.MapHub<InstanceManagementHub>("/api/internal/managementhub");

app.MapDefaultControllerRoute().WithStaticAssets();

// publish scalar api docs endpoint in dev, e.g. https://localhost:44361/api/docs

// Enable middleware to serve generated OpenAPI document as a JSON endpoint.
app.MapOpenApi();

// Enable middleware to serve API docs
app.MapScalarApiReference("/api/docs/", options =>
{
    options
                    .WithTitle("Certify Management Hub API")
                    .WithTheme(ScalarTheme.Solarized)
                    .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient)
                    .WithOpenApiRoutePattern("/openapi/v1.json");

});

AddSystemStatusItem(
    SystemStatusCategories.HUB_API,
    SystemStatusKeys.HUB_API_STARTUP_APIDOCS,
    title: "API Docs UI enabled",
    description: $"Hub API docs available at /api/docs"
);

// configure initialization of UI status hub, backend management hub etc

var statusHubContext = app.Services.GetRequiredService<IHubContext<UserInterfaceStatusHub>>();

if (statusHubContext == null)
{
    throw new Exception("Status Hub not registered");
}

// setup signalr message forwarding, message received from internal service will be resent to our connected clients via our own SignalR hub
var statusReporting = new UserInterfaceStatusHubReporting(statusHubContext);

// wire up internal service to our hub
var managementServerClient = app.Services.GetService<DirectManagementServerClient>();

var certifyManager = app.Services.GetRequiredService<ICertifyManager>();

certifyManager.EnableManagementHubBackend(isDirectHubBackend: true);

// initialize the CertifyManager instance, this includes initial setup of hub assigned instance id
await certifyManager.Init();

// setup direct management client, this tells the primary backend CertifyManager instance to talk directly to the management hub instead of via SignalR
var directServerClient = app.Services.GetRequiredService<IManagementServerClient>();

certifyManager.SetDirectManagementClient(directServerClient);

// wire up status reporting, include management hub cached state handlers for request progress state updates and item updates
certifyManager.SetStatusReporting(statusReporting);

var hubStateProvider = app.Services.GetRequiredService<IInstanceManagementStateProvider>();

// inform the management hub of our assigned backend instance id, so we can tell when we are interacting with the mgmt hub vs a normal instance
hubStateProvider.SetManagementHubInstanceId(certifyManager.GetManagedInstanceInfo().InstanceId);

statusReporting.OnRequestProgressStateUpdated += (RequestProgressState state) =>
{

};

statusReporting.OnManagedCertificateUpdated += (ManagedCertificate item) =>
{
    if (item.InstanceId != null)
    {
        var mgmtHubState = app.Services.GetRequiredService<IInstanceManagementStateProvider>();
        mgmtHubState.UpdateCachedManagedInstanceItem(item.InstanceId, item);
    }
};

// start the server and watch for shutdown signals

app.Start();

app.Logger.LogInformation($"Server started {string.Join(";", app.Urls)}");

AddSystemStatusItem(
    SystemStatusCategories.HUB_API,
    SystemStatusKeys.HUB_API_STARTUP_URL,
    title: "API Urls Allocated",
    description: $"Hub API available at {string.Join(";", app.Urls)}"
);

foreach (var statusItem in _systemStatusItems)
{
    hubStateProvider.AddOrUpdateSystemStatusItem(statusItem);

    if (statusItem.HasError)
    {
        app.Logger.LogError($"{statusItem.Key} - {statusItem.Title} - {statusItem.Description}");
    }
    else if (statusItem.HasWarning)
    {
        app.Logger.LogWarning($"{statusItem.Key} - {statusItem.Title} - {statusItem.Description}");
    }
    else
    {
        app.Logger.LogInformation($"{statusItem.Key} - {statusItem.Title} - {statusItem.Description}");
    }
}

app.WaitForShutdown();
